/*******************************************************************************
 * Copyright (c) 2008 CEA LIST.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     CEA LIST - initial API and implementation
 *******************************************************************************/
package com.cea.papyrus.diagram.classdiagram.tabbedproperties.composite;

import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;
import org.eclipse.uml2.common.edit.command.ChangeCommand;
import org.eclipse.uml2.uml.OpaqueExpression;

import com.cea.papyrus.core.PapyrusPlugin;
import com.cea.papyrus.core.commands.EMFtoGEFWrapCommand;
import com.cea.papyrus.core.editor.CommandStackUtils;
import com.cea.papyrus.core.preference.OpaqueLanguagesPreferenceConstants;
import com.cea.papyrus.core.tabbedproperties.PropertyViewSection;
import com.cea.papyrus.extensionpoints.editors.configuration.IDirectEditorConfiguration;
import com.cea.papyrus.extensionpoints.editors.utils.DirectEditorsUtil;
import com.cea.papyrus.umlutils.OpaqueExpressionUtil;


/**
 * composite that holds the value of a body, and the selector of language
 */
public class SmallOpaqueExpressionComposite extends PropertyViewSection {

	/** text area */
	protected SourceViewer textArea;

	/** CCombo */
	protected CCombo comboLanguage;

	/** parent composite for text and ccombo */
	protected Composite mainComposite;

	/** combo selection listener */
	protected LanguageSelectionListener comboListener;

	/** list of available languages */
	private List<String> languages;

	/** current edited Language */
	private String currentLanguage;

	/** Edited Opaque Expression */
	protected OpaqueExpression opaqueExpression;

	/** Key listener for 'Enter' Key */
	protected KeyListener keyListener;

	/** Focus listener for the text area */
	protected FocusListener focusListener;

	/** Composite where to display error messages*/
	private CLabel errorLabel;

	/** Warning image for validation label */
	protected final Image WARNING_IMAGE = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_WARN_TSK);

	/** input validator used to validate entered text */
	private IInputValidator validator;


	/**
	 * Creates a new SmallOpaqueExpressionComposite.
	 */
	public SmallOpaqueExpressionComposite() {
		super();
		comboListener = new LanguageSelectionListener();
		keyListener = new EnterKeyListener();
		focusListener = new TextFocusListener();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void createControls(Composite parent, TabbedPropertySheetPage tabbedPropertySheetPage) {
		super.createControls(parent, tabbedPropertySheetPage);
		mainComposite = getWidgetFactory().createComposite(parent);
		GridLayout layout = new GridLayout(2, false);
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		mainComposite.setLayout(layout);
		textArea = new SourceViewer(mainComposite, null, SWT.BORDER);
		Control textComposite = textArea.getControl();
		GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
		data.heightHint = SWT.DEFAULT; 
		textComposite.setLayoutData(data);
		textComposite.addKeyListener(keyListener);
		textComposite.addFocusListener(focusListener);
		comboLanguage = getWidgetFactory().createCCombo(mainComposite, SWT.BORDER | SWT.READ_ONLY);
		comboLanguage.setItems(getLanguages().toArray(new String[] {}));
		// look for extension point and preferences for language
		comboLanguage.addSelectionListener(comboListener);
		data.widthHint = 60;
		data = new GridData(SWT.FILL, SWT.FILL, false, true);
		data.grabExcessHorizontalSpace = false;
		comboLanguage.setLayoutData(data);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void dispose() {
		if(textArea!=null && textArea.getControl()!=null && !textArea.getControl().isDisposed()) {
			textArea.getControl().removeKeyListener(keyListener);
			textArea.getControl().removeFocusListener(focusListener);
		}
		if(comboLanguage!=null && !comboLanguage.isDisposed()) {
			comboLanguage.removeSelectionListener(comboListener);
		}
		super.dispose();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void refresh() {
		super.refresh();
		if(getOpaqueExpression()!=null) {
			String language = getCurrentLanguage();
			textArea.unconfigure();

			// add completion processor key listener (ctrl+space keys) 
			textArea.appendVerifyKeyListener(new LabelKeyListener(textArea));

			IDirectEditorConfiguration configuration = DirectEditorsUtil.findEditorConfiguration(language, OpaqueExpression.class.getCanonicalName());
			configuration.preEditAction(getOpaqueExpression());
			configuration.setLanguage(language);
			textArea.configure(configuration.getSourceViewerConfiguration());
			validator = configuration.getInputValidator();

			Document document;
			if(DirectEditorsUtil.hasSpecificEditorConfiguration(getCurrentLanguage(), OpaqueExpression.class.getCanonicalName())) {
				document = new Document(configuration.getTextToEdit(getOpaqueExpression()));
			} else {
				document = new Document(OpaqueExpressionUtil.getBodyForLanguage(getOpaqueExpression(), getCurrentLanguage()));
			}
			
			textArea.setDocument(document);
			document.addDocumentListener(new DocumentListener());

			comboLanguage.select(getLanguages().indexOf(language));
		}
	}

	/**
	 * Returns the current edited language. if no one is selected, takes the first one in the list of the preference store
	 * @return the current edited language
	 */
	private String getCurrentLanguage() {
		if(currentLanguage == null) {
			currentLanguage = getLanguages().get(0);
		}
		return currentLanguage;
	}

	/**
	 * Inits the language list
	 */
	protected List<String> getLanguages() {
		if(languages ==null) {
			languages = new ArrayList<String>();
			
			// get languages from extension points
			languages.addAll(DirectEditorsUtil.getLanguages("org.eclipse.uml2.uml.OpaqueExpression"));
			
			//Get the preference  from PreferenceStore to add user defined languages
			IPreferenceStore store = PapyrusPlugin.getDefault().getPreferenceStore();		
			String userLanguages = store.getString(OpaqueLanguagesPreferenceConstants.P_LANGUAGES_NAME);
			StringTokenizer st = new StringTokenizer(userLanguages, ",");

			int count = st.countTokens();

			for(int i=0; i<count; i++){
				String lang = st.nextToken().trim();
				if(!languages.contains(lang)) {
					languages.add(lang);
				}
			}
		}
		return languages;
	}

	/**
	 * Returns the current edited Opaque expression
	 * @return the opaqueExpression
	 */
	public OpaqueExpression getOpaqueExpression() {
		return opaqueExpression;
	}

	/**
	 * Sets the edited Opaque expression
	 * @param opaqueExpression the opaqueExpression to edit
	 */
	public void setOpaqueExpression(OpaqueExpression opaqueExpression) {
		this.opaqueExpression = opaqueExpression;
	}

	/**
	 * Returns the main composite for this section
	 * @return the mainComposite
	 */
	public Composite getMainComposite() {
		return mainComposite;
	}

	/**
	 * Apply the specified text value to the opaquae expression
	 * @param text the text to save
	 */
	protected void applyNewTextValue(final String text) {
		final AdapterFactoryEditingDomain emfDomain = getModelManager().getDiResourceSet().getEditingDomain();
		EMFtoGEFWrapCommand command = new EMFtoGEFWrapCommand(emfDomain) {
			@Override
			public org.eclipse.emf.common.command.Command createEMFCommand() {
				return new ChangeCommand(emfDomain, new Runnable() {
					public void run() {
						if(DirectEditorsUtil.hasSpecificEditorConfiguration(getCurrentLanguage(), OpaqueExpression.class.getCanonicalName())) {
							IDirectEditorConfiguration configuration = DirectEditorsUtil.findEditorConfiguration(getCurrentLanguage(), OpaqueExpression.class.getCanonicalName());
							configuration.postEditAction(getOpaqueExpression(), textArea.getDocument().get());
						} else {
							OpaqueExpressionUtil.setBodyForLanguage(getOpaqueExpression(), getCurrentLanguage(), textArea.getDocument().get());
						}
					}
				}, "Change Opaque Expression value", "Changes the Value of the Opaque Expression");
			}
		};
		CommandStackUtils.getCommandStack().execute(command);
	}

	/**
	 * Listener for the language combo composite 
	 */
	protected class LanguageSelectionListener implements SelectionListener {

		/**
		 * {@inheritDoc}
		 */
		public void widgetDefaultSelected(SelectionEvent e) {
		}

		/**
		 * {@inheritDoc}
		 */
		public void widgetSelected(SelectionEvent e) {
			final int selectionIndex = ((CCombo)e.widget).getSelectionIndex();
			currentLanguage = getLanguages().get(selectionIndex);
			refresh();
		}
	}

	/**
	 * Listener for the text area. If focus is lost, it applies the modifications to the element
	 */
	protected class TextFocusListener implements FocusListener {
		/**
		 * {@inheritDoc}
		 */
		public void focusGained(FocusEvent e) {

		}
		/**
		 * {@inheritDoc}
		 */
		public void focusLost(FocusEvent e) {
			// get the new value
			final String text = ( ((StyledText)e.widget).getText() != null ) ? ((StyledText)e.widget).getText().trim() : null;
			if(text == null) {
				// should never happened
				return;
			}
			applyNewTextValue(text);
			refresh();
		}
	}

	/**
	 * Key listener for the "enter" event
	 */
	protected class EnterKeyListener implements KeyListener {

		/**
		 * {@inheritDoc}
		 */
		public void keyPressed(KeyEvent e) {

		}

		/**
		 * {@inheritDoc}
		 */
		public void keyReleased(KeyEvent e) {
			// do nothing
			if(SWT.CR==e.keyCode) {
				// applyNewTextValue(((StyledText)e.widget).getText());
				// lose focus so focus lost listener is called
				comboLanguage.setFocus();
			}
		}
	}

	/**
	 * Key Listener for Completion
	 */
	private class LabelKeyListener implements VerifyKeyListener {

		/** viewer that listens for key event */
		SourceViewer viewer;

		/**
		 * Creates a new LabelKeyListener.
		 * @param viewer the viewer that listens for keys
		 */
		public LabelKeyListener(SourceViewer viewer) {
			this.viewer = viewer;
		}

		/**
		 * {@inheritDoc}
		 */
		public void verifyKey(VerifyEvent event) {
			if ((event.stateMask == SWT.CTRL) && (event.character == ' ')) {
				if (viewer.canDoOperation(ISourceViewer.CONTENTASSIST_PROPOSALS)) {
					viewer.doOperation(ISourceViewer.CONTENTASSIST_PROPOSALS);
				}
				event.doit = false;
			} else if(event.character == SWT.CR) {
				event.doit=false;
			} 
		}
	}

	/**
	 * Listener for document modification
	 */
	private class DocumentListener implements IDocumentListener {

		/**
		 * {@inheritDoc}
		 */
		public void documentAboutToBeChanged(DocumentEvent event) {
		}
		
		/**
		 * {@inheritDoc}
		 */
		public void documentChanged(DocumentEvent event) {
			validateInput();
		}
	}

	/**
	 * Sets the error label where to display the result of validation of the text entered in the sourceviewer
	 * @param validationLabel the label where the text of the error is displayed
	 */
	public void setErrorLabel(CLabel validationLabel) {
		errorLabel = validationLabel;
	}

	/**
	 * Validates the input.
	 * <p>
	 * The default implementation of this framework method delegates the request
	 * to the supplied input validator object; if it finds the input invalid,
	 * the error message is displayed in the dialog's message line. This hook
	 * method is called whenever the text changes in the input field.
	 * </p>
	 */
	protected void validateInput() {
		String errorMessage = null;
		if (validator != null) {
			errorMessage = validator.isValid(textArea.getDocument().get());
		}
		// Bug 16256: important not to treat "" (blank error) the same as null
		// (no error)
		setErrorMessage(errorMessage);
	}

	/**
	 * Sets the new Error Message to display
	 * @param string the error message
	 */
	private void setErrorMessage(String string) {
		if(errorLabel == null) {
			return;
		}
		errorLabel.setText(string);
		if(string == null || "".equals(string)) {
			errorLabel.setImage(null);
		} else {
			errorLabel.setImage(WARNING_IMAGE);
		}
	}
}
